package org.type4neo.bytecode;

import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;

import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.Relationship;
import org.type4neo.TypedNode;
import org.type4neo.TypedRelationship;
import org.type4neo.imp.TypedNodeImp;
import org.type4neo.imp.TypedPropertyContainer;
import org.type4neo.imp.TypedRelationshipImp;

public abstract class DynamicTyper {
	private static final String SUFFIX = "_TypedProxy";
	public static final String PROTOTYPE_PROPERTY_NAME = "prototype___";
	public static Map<String, Class<?>> classMap = new HashMap<String, Class<?>>();
	private static Map<String,Class<?>> wrapperTypes;
	private static Map<String,String> primitiveUnboxers;
	private static Map<String,String>  primitiveDefaults;
		
	static {
		wrapperTypes = new HashMap<String, Class<?>>();
		primitiveUnboxers = new HashMap<String, String>();
		primitiveDefaults = new HashMap<String, String>();
		
		wrapperTypes.put(boolean.class.getName(), Boolean.class);
		primitiveUnboxers.put(boolean.class.getName(), "booleanValue");
		primitiveDefaults.put(boolean.class.getName(), "false");
		wrapperTypes.put(byte.class.getName(), Byte.class);
		primitiveUnboxers.put(byte.class.getName(), "byteValue");
		primitiveDefaults.put(byte.class.getName(), "(byte) 0");
		wrapperTypes.put(char.class.getName(), Character.class);
		primitiveUnboxers.put(char.class.getName(), "charValue");
		primitiveDefaults.put(char.class.getName(), "(char) 0");
		wrapperTypes.put(double.class.getName(), Double.class);
		primitiveUnboxers.put(double.class.getName(), "doubleValue");
		primitiveDefaults.put(double.class.getName(), "0d");
		wrapperTypes.put(float.class.getName(), Float.class);
		primitiveUnboxers.put(float.class.getName(), "floatValue");
		primitiveDefaults.put(float.class.getName(), "0f");
		wrapperTypes.put(int.class.getName(), Integer.class);
		primitiveUnboxers.put(int.class.getName(), "intValue");
		primitiveDefaults.put(int.class.getName(), "0");
		wrapperTypes.put(long.class.getName(), Long.class);
		primitiveUnboxers.put(long.class.getName(), "longValue");
		primitiveDefaults.put(long.class.getName(), "(long) 0");
		wrapperTypes.put(short.class.getName(), Short.class);
		primitiveUnboxers.put(short.class.getName(), "shortValue");
		primitiveDefaults.put(short.class.getName(), "(short) 0");
	}

	private static Object as(PropertyContainer pc, Class<?> interfaceClass) {
		if (interfaceClass == null) throw new NullPointerException("Interface class can't be null");
		if (!interfaceClass.isInterface()) throw new IllegalArgumentException("Given class must be an Instance class");
		if (pc == null) throw new NullPointerException("Property container can't be null");
		TypedPropertyContainer typer = null;
		try {
			typer = DynamicTyper.getInstanceForType(interfaceClass);
			
		} catch (Exception e) {
			throw new RuntimeException("Couldn't build proxy for interface " + interfaceClass.getName(), e);
		}
		if (typer == null) throw new RuntimeException("Couldn't build proxy for interface " + interfaceClass.getName() + " [null]");
		typer.setPropertyContainer(pc);
		return typer;
	}
	
	private static TypedPropertyContainer getInstanceForType(
			Class<?> interfaceClass) throws Exception {
		Class<?> theClass = getClassFor(interfaceClass);
		return (TypedPropertyContainer) theClass.newInstance();
	}

	private static Class<?> getClassFor(Class<?> interfaceClass) throws Exception {
		Class<?> temp = classMap.get(interfaceClass.getName());
		if (temp == null) {
			temp = buildClassFor(interfaceClass);
			classMap.put(interfaceClass.getName(), temp);
		}
		return temp;
	}

	private static Class<?> buildClassFor(Class<?> interfaceClass) throws Exception {
		ClassPool pool = ClassPool.getDefault();
		
		CtClass superClass;
		if (TypedNode.class.isAssignableFrom(interfaceClass)) {
			superClass = pool.get(TypedNodeImp.class.getName());
		} else if (TypedRelationship.class.isAssignableFrom(interfaceClass)) {
			superClass = pool.get(TypedRelationshipImp.class.getName());
		} else {
			throw new IllegalArgumentException("Given interface doesn't extend TypedNode or TypedRelationship");
		}
		
		try {
			CtClass temp = pool.get(interfaceClass.getName() +  SUFFIX);
			return temp.toClass();
		} catch (NotFoundException e) {}
		
		CtClass interfaceCtClass = pool.get(interfaceClass.getName());
		CtClass newClass = pool.makeClass(interfaceClass.getName() +  SUFFIX);
		newClass.setSuperclass(superClass);
		
		Set<String> requiredInterfacesForCopy = new HashSet<String>();
		Map<String,String> fieldsToCopy = new HashMap<String, String>();
		
		// Implementation of given interface
		newClass.addInterface(interfaceCtClass);
		for (CtMethod m : interfaceCtClass.getMethods()) {
			String dc = m.getDeclaringClass().getName();
			//System.out.print(interfaceClass.getName() + " --> " + m.getName() + "@" + dc + ": ");
			if (dc.equals("java.lang.Object") ||
					dc.startsWith("org.neo4j.") || 
					dc.equals(TypedNode.class.getName()) ||
					dc.equals(TypedPropertyContainer.class.getName()) ||
					dc.equals(TypedRelationship.class.getName())) {
				//System.out.println("skipped");
				continue;
			}
			//System.out.println(" added");
			requiredInterfacesForCopy.add(dc);
			String fieldName = getFieldNameFromMethodName(m.getName());
			fieldsToCopy.put(fieldName, dc);
			if (m.getName().startsWith("is")) {
				CtMethod imp = CtNewMethod.copy(m, newClass, null);
				if (m.getReturnType().getName().equals(Boolean.class.getName())) {
					imp.setBody("{return (Boolean) this.pc.getProperty(\"" + fieldName + "\")}");
				} else {
					imp.setBody("if (this.hasProperty(\"" + fieldName + "\")) {return ((Boolean) this.getProperty(\"" + fieldName + "\")).booleanValue();} else { return false; }");
				}
				newClass.addMethod(imp);
			} else if (m.getName().startsWith("get")) {
				// Getter
				CtMethod imp = CtNewMethod.copy(m, newClass, null);
				CtClass rt = m.getReturnType();
				if (rt.isPrimitive()) {
					imp.setBody("if (this.hasProperty(\"" + fieldName + "\")) {return ((" + wrapperTypes.get(rt.getName()).getName() + ") this.getProperty(\"" + fieldName + "\"))." + primitiveUnboxers.get(rt.getName()) + "();} else { return " + primitiveDefaults.get(rt.getName()) + ";}");
				} else if (rt.isArray()) {
					imp.setBody("if (this.hasProperty(\"" + fieldName + "\")) {return ((" + rt.getComponentType().getName() + "[]) this.getProperty(\"" + fieldName + "\"));} else {return null;}");
				} else if (rt.getName().equals(Date.class.getName())) {
					imp.setBody("if (this.hasProperty(\"" + fieldName + "\")) {return new java.util.Date(((Long) this.getProperty(\"" + fieldName + "\")).longValue());} else { return null; }");
				} else {
					imp.setBody("if (this.hasProperty(\"" + fieldName + "\")) {return ((" + rt.getName() + ") this.getProperty(\"" + fieldName + "\"));} else { return null; }");
				}
				newClass.addMethod(imp);
			} else if (m.getName().startsWith("set")) {
				// Setter
				CtMethod imp = CtNewMethod.copy(m, newClass, null);
				CtClass[] params = m.getParameterTypes();
				if (params.length != 1) throw new UnsupportedOperationException("Typed proxies can only be implemented for Bean compliant interfaces. Don't know how to handle a setter method that has " + params.length + " parameter");
				CtClass param = params[0];
				if (param.isPrimitive()) {
					imp.setBody("{this.setProperty(\"" + fieldName + "\", new " + wrapperTypes.get(param.getName()).getName() + " ($1));}");
				} else if (param.getName().equals(Date.class.getName())) {
					imp.setBody("if ($1 != null) {this.setProperty(\"" + fieldName + "\", new Long(((java.util.Date) $1).getTime()));}");
				} else {
					imp.setBody("if ($1 != null) {this.setProperty(\"" + fieldName + "\", $1);}");
				}
				newClass.addMethod(imp);
			}
		}
		
		StringBuffer src;
		if (!requiredInterfacesForCopy.isEmpty()) {
			// Implementation of: checkObjectForCopy(Object obj);
			src = new StringBuffer();
			src.append("private void checkObjectForCopy(Object obj) {");
			src.append("if (obj == null) { throw new NullPointerException(\"Parameter object for copyTo/copyFrom can't be null\"); }");
			src.append("if (");
			boolean first = true;
			for (Iterator<String> i = requiredInterfacesForCopy.iterator(); i.hasNext();) {
				if (!first) src.append(" && ");
				else first = false;
				src.append("(obj instanceof " + i.next() + ")");
			}
			src.append(") {return; } else { throw new java.lang.IllegalArgumentException(\"Can't copy from/to an object of type \" + obj.getClass().getName() + \". Valid objects must implement interface(s): ");
			first = true;
			for (Iterator<String> i = requiredInterfacesForCopy.iterator(); i.hasNext();) {
				if (!first) src.append(", ");
				else first = false;
				src.append(i.next());
			}
			src.append("\"); }");
			src.append("}");
			//System.out.println(src.toString());
			newClass.addMethod(CtNewMethod.make(src.toString(), newClass));
		}
		
		// Implementation of: public void copyFrom(Object obj);
		src = new StringBuffer();
		src.append("public void copyFrom(Object obj) {");
		if (!requiredInterfacesForCopy.isEmpty()) src.append("this.checkObjectForCopy(obj);");
		for (Iterator<Entry<String, String>> i = fieldsToCopy.entrySet().iterator(); i.hasNext();) {
			Entry<String, String> t = i.next();
			String fName = t.getKey().substring(0, 1).toUpperCase() + t.getKey().substring(1);
			if (fName.equals("Id")) {
				src.append("((" + t.getValue() + ") obj).set" + fName + "(this.get" + fName + "());");
			} else {
				src.append("this.set" + fName + "(((" + t.getValue() + ") obj).get" + fName + "());");
			}
		}
		src.append("}");
		//System.out.println(src.toString());
		newClass.addMethod(CtNewMethod.make(src.toString(), newClass));
		
		// Implementation of: public abstract void copyTo(Object obj);
		src = new StringBuffer();
		src.append("public void copyTo(Object obj) {");
		if (!requiredInterfacesForCopy.isEmpty()) src.append("this.checkObjectForCopy(obj);");
		for (Iterator<Entry<String, String>> i = fieldsToCopy.entrySet().iterator(); i.hasNext();) {
			Entry<String, String> t = i.next();
			String fName = t.getKey().substring(0, 1).toUpperCase() + t.getKey().substring(1);
			src.append("((" + t.getValue() + ") obj).set" + fName + "(this.get" + fName + "());");
		}
		src.append("}");
		//System.out.println(src.toString());
		newClass.addMethod(CtNewMethod.make(src.toString(), newClass));
		
		return newClass.toClass();
	}

	private static String getFieldNameFromMethodName(String m) {
		if (m.startsWith("is")) {
			return m.substring(2, 3).toLowerCase() + m.substring(3);
		} else if (m.startsWith("get")) {
			return m.substring(3, 4).toLowerCase() + m.substring(4);
		} else if (m.startsWith("set")) {
			return m.substring(3, 4).toLowerCase() + m.substring(4);
		} else {
			throw new UnsupportedOperationException("Typed proxies can only be implemented for Bean compliant interfaces. Don't know how to handle a method called: " + m);
		}
	}

	public static TypedNode wrapNode(Node node) {
		String typeName = TypedNode.class.getName();
		if (node.hasProperty(PROTOTYPE_PROPERTY_NAME)) {
			typeName = (String) node.getProperty(PROTOTYPE_PROPERTY_NAME); 
		}
		try {
			Class<?> clazz = Class.forName(typeName);
			return (TypedNode) as(node, clazz);
		} catch (ClassNotFoundException e) {
			throw new RuntimeException("Couldn't find interface called " + typeName, e);
		}
	}

	public static TypedRelationship wrapRelationship(Relationship relationship) {
		String typeName = TypedRelationship.class.getName();
		if (relationship.hasProperty(PROTOTYPE_PROPERTY_NAME)) {
			typeName = (String) relationship.getProperty(PROTOTYPE_PROPERTY_NAME); 
		}
		try {
			Class<?> clazz = Class.forName(typeName);
			return (TypedRelationship) as(relationship, clazz);
		} catch (ClassNotFoundException e) {
			throw new RuntimeException("Couldn't find interface called " + typeName, e);
		}
	}

}
